diff options
Diffstat (limited to 'app/[lng]/evcp/(evcp)/(procurement)')
14 files changed, 504 insertions, 306 deletions
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx new file mode 100644 index 00000000..f460f570 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next'
+import { getBiddingsForFailure } from '@/lib/bidding/service'
+import { GetBiddingsSchema, searchParamsCache } from '@/lib/bidding/validation'
+import { BiddingsFailureTable } from '@/lib/bidding/failure/biddings-failure-table'
+
+export const metadata: Metadata = {
+ title: '유찰입찰',
+ description: '유찰된 입찰 내역을 확인하고 재입찰을 진행할 수 있습니다.',
+}
+
+interface BiddingFailurePageProps {
+ searchParams: Promise<Record<string, string | string[] | undefined>>
+}
+
+export default async function BiddingFailurePage({
+ searchParams,
+}: BiddingFailurePageProps) {
+ // URL 파라미터 검증
+ const searchParamsResolved = await searchParams
+ const search = searchParamsCache.parse(searchParamsResolved)
+
+ // 데이터 조회
+ const biddingsPromise = getBiddingsForFailure(search)
+
+ return (
+ <div className="flex flex-col gap-4 p-4">
+ <div className="flex items-center justify-between">
+ <div>
+ <h1 className="text-2xl font-bold tracking-tight">유찰입찰</h1>
+ <p className="text-muted-foreground">
+ 유찰된 입찰 내역을 확인하고 재입찰을 진행할 수 있습니다.
+ </p>
+ </div>
+ </div>
+
+ <BiddingsFailureTable promises={Promise.all([biddingsPromise])} />
+ </div>
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx new file mode 100644 index 00000000..0d725bbf --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next'
+import { getBiddingsForReceive } from '@/lib/bidding/service'
+import { GetBiddingsSchema, searchParamsCache } from '@/lib/bidding/validation'
+import { BiddingsReceiveTable } from '@/lib/bidding/receive/biddings-receive-table'
+
+export const metadata: Metadata = {
+ title: '입찰서접수및마감',
+ description: '입찰서 접수 및 마감 현황을 확인하고 개찰을 진행할 수 있습니다.',
+}
+
+interface BiddingReceivePageProps {
+ searchParams: Promise<Record<string, string | string[] | undefined>>
+}
+
+export default async function BiddingReceivePage({
+ searchParams,
+}: BiddingReceivePageProps) {
+ // URL 파라미터 검증
+ const searchParamsResolved = await searchParams
+ const search = searchParamsCache.parse(searchParamsResolved)
+
+ // 데이터 조회
+ const biddingsPromise = getBiddingsForReceive(search)
+
+ return (
+ <div className="flex flex-col gap-4 p-4">
+ <div className="flex items-center justify-between">
+ <div>
+ <h1 className="text-2xl font-bold tracking-tight">입찰서접수및마감</h1>
+ <p className="text-muted-foreground">
+ 입찰서 접수 현황을 확인하고 개찰을 진행할 수 있습니다.
+ </p>
+ </div>
+ </div>
+
+ <BiddingsReceiveTable promises={Promise.all([biddingsPromise])} />
+ </div>
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx new file mode 100644 index 00000000..40b714de --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next'
+import { getBiddingsForSelection } from '@/lib/bidding/service'
+import { GetBiddingsSchema, searchParamsCache } from '@/lib/bidding/validation'
+import { BiddingsSelectionTable } from '@/lib/bidding/selection/biddings-selection-table'
+
+export const metadata: Metadata = {
+ title: '입찰선정',
+ description: '개찰 이후 입찰가를 확인하고 낙찰업체를 선정할 수 있습니다.',
+}
+
+interface BiddingSelectionPageProps {
+ searchParams: Promise<Record<string, string | string[] | undefined>>
+}
+
+export default async function BiddingSelectionPage({
+ searchParams,
+}: BiddingSelectionPageProps) {
+ // URL 파라미터 검증
+ const searchParamsResolved = await searchParams
+ const search = searchParamsCache.parse(searchParamsResolved)
+
+ // 데이터 조회
+ const biddingsPromise = getBiddingsForSelection(search)
+
+ return (
+ <div className="flex flex-col gap-4 p-4">
+ <div className="flex items-center justify-between">
+ <div>
+ <h1 className="text-2xl font-bold tracking-tight">입찰선정</h1>
+ <p className="text-muted-foreground">
+ 개찰 이후 입찰가를 확인하고 낙찰업체를 선정할 수 있습니다.
+ </p>
+ </div>
+ </div>
+
+ <BiddingsSelectionTable promises={Promise.all([biddingsPromise])} />
+ </div>
+ )
+}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx new file mode 100644 index 00000000..0321d273 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx @@ -0,0 +1,77 @@ +"use client"
+
+import * as React from "react"
+import { usePathname, useRouter } from "next/navigation"
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
+
+interface BiddingTabsProps {
+ id: string
+}
+
+export function BiddingTabs({ id }: BiddingTabsProps) {
+ const pathname = usePathname()
+ const router = useRouter()
+
+ const tabs = React.useMemo(() => [
+ {
+ key: "info",
+ label: "입찰 기본 정보",
+ href: `/evcp/bid/${id}/info`,
+ },
+ {
+ key: "companies",
+ label: "입찰 업체",
+ href: `/evcp/bid/${id}/companies`,
+ },
+ {
+ key: "items",
+ label: "입찰 품목",
+ href: `/evcp/bid/${id}/items`,
+ },
+ {
+ key: "schedule",
+ label: "입찰 계획",
+ href: `/evcp/bid/${id}/schedule`,
+ },
+ ], [id])
+
+ // 현재 활성 탭 결정
+ const activeTab = React.useMemo(() => {
+ if (!pathname) return "info"
+
+ // pathname에서 lng 부분 제거 (예: /en/evcp/bid/10 -> /evcp/bid/10)
+ const normalizedPath = pathname.replace(/^\/[^/]+/, '') || pathname
+
+ // 기본 페이지는 info로 처리
+ if (normalizedPath === `/evcp/bid/${id}` || normalizedPath.endsWith(`/bid/${id}`)) {
+ return "info"
+ }
+
+ const matchedTab = tabs.find(tab => normalizedPath.includes(`/${tab.key}`))
+ return matchedTab?.key || "info"
+ }, [pathname, id, tabs])
+
+ return (
+ <div className="flex items-center gap-1">
+ {tabs.map((tab) => {
+ const isActive = activeTab === tab.key
+ return (
+ <Button
+ key={tab.key}
+ variant={isActive ? "secondary" : "ghost"}
+ size="default"
+ className={cn(
+ "text-md px-3 py-1 h-7",
+ isActive && "bg-secondary"
+ )}
+ onClick={() => router.push(tab.href)}
+ >
+ {tab.label}
+ </Button>
+ )
+ })}
+ </div>
+ )
+}
+
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx new file mode 100644 index 00000000..f1699665 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from 'next/navigation'
+import { getBiddingById } from "@/lib/bidding/service"
+import { Bidding } from "@/db/schema/bidding"
+import { Button } from "@/components/ui/button"
+import { ArrowLeft } from "lucide-react"
+import Link from "next/link"
+import { BiddingCompaniesEditor } from "@/components/bidding/manage/bidding-companies-editor"
+import { BiddingTabs } from "../bidding-tabs"
+
+// 메타데이터 생성
+export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) {
+ const { lng, id } = await params
+ const parsedId = parseInt(id)
+ if (isNaN(parsedId)) return { title: '입찰 업체 및 담당자 관리' }
+
+ try {
+ const bidding = await getBiddingById(parsedId)
+ return {
+ title: bidding ? `${bidding.title} - 입찰 업체 및 담당자 관리` : '입찰 업체 및 담당자 관리',
+ }
+ } catch {
+ return { title: '입찰 업체 및 담당자 관리' }
+ }
+}
+
+interface PageProps {
+ params: Promise<{ lng: string; id: string }>
+}
+
+export default async function BiddingCompaniesPage({ params }: PageProps) {
+ const { lng, id } = await params
+ const parsedId = parseInt(id)
+
+ if (isNaN(parsedId)) {
+ notFound()
+ }
+
+ const bidding: Bidding | null = await getBiddingById(parsedId)
+
+ if (!bidding) {
+ notFound()
+ }
+
+ return (
+ <div className="container py-6 space-y-6">
+ {/* 헤더 */}
+ <div className="flex justify-between items-center">
+ <div className="flex items-center gap-4">
+ <div>
+ <h1 className="text-3xl font-bold tracking-tight">
+ 입찰 업체 및 담당자 관리
+ </h1>
+ <p className="text-muted-foreground mt-2">
+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title}
+ </p>
+ </div>
+ </div>
+ <Link href={`/${lng}/evcp/bid/${id}`} passHref>
+ <Button variant="outline" className="flex items-center">
+ <ArrowLeft className="mr-2 h-4 w-4" />
+ 입찰 관리로 돌아가기
+ </Button>
+ </Link>
+ </div>
+
+ {/* 탭 네비게이션 */}
+ <div>
+ <BiddingTabs id={id} />
+ </div>
+
+ {/* 입찰 업체 및 담당자 에디터 */}
+ <BiddingCompaniesEditor biddingId={parsedId} />
+ </div>
+ )
+}
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx deleted file mode 100644 index 4dc36e20..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Suspense } from 'react' -import { notFound } from 'next/navigation' -import { getBiddingDetailData } from '@/lib/bidding/detail/service' -import { BiddingDetailContent } from '@/lib/bidding/detail/table/bidding-detail-content' - -// 메타데이터 생성 -export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params - const parsedId = parseInt(id) - if (isNaN(parsedId)) return { title: '입찰 관리상세' } - - try { - const detailData = await getBiddingDetailData(parsedId) - return { - title: detailData.bidding ? `${detailData.bidding.title} - 입찰 관리상세` : '입찰 관리상세', - } - } catch { - return { title: '입찰 관리상세' } - } -} - -interface PageProps { - params: Promise<{ id: string }> -} - -export default async function Page({ params }: PageProps) { - const { id } = await params - const parsedId = parseInt(id) - - if (isNaN(parsedId)) { - notFound() - } - - // 통합 데이터 로딩 함수 사용 - const detailData = await getBiddingDetailData(parsedId) - - if (!detailData.bidding) { - notFound() - } - - return ( - <Suspense fallback={<div className="p-8">로딩 중...</div>}> - <BiddingDetailContent - bidding={detailData.bidding} - quotationDetails={detailData.quotationDetails} - quotationVendors={detailData.quotationVendors} - prItems={detailData.prItems} - /> - </Suspense> - ) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx new file mode 100644 index 00000000..7281d206 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from 'next/navigation' +import { getBiddingById } from "@/lib/bidding/service" +import { Bidding } from "@/db/schema/bidding" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +import { BiddingBasicInfoEditor } from "@/components/bidding/manage/bidding-basic-info-editor" +import { BiddingTabs } from "../bidding-tabs" + +// 메타데이터 생성 +export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) { + const { id } = await params + const parsedId = parseInt(id) + if (isNaN(parsedId)) return { title: '입찰 기본 정보 관리' } + + try { + const bidding = await getBiddingById(parsedId) + return { + title: bidding ? `${bidding.title} - 입찰 기본 정보 관리` : '입찰 기본 정보 관리', + } + } catch { + return { title: '입찰 기본 정보 관리' } + } +} + +interface PageProps { + params: Promise<{ lng: string; id: string }> +} + +export default async function BiddingBasicInfoPage({ params }: PageProps) { + const { lng, id } = await params + const parsedId = parseInt(id) + + if (isNaN(parsedId)) { + notFound() + } + + const bidding: Bidding | null = await getBiddingById(parsedId) + + if (!bidding) { + notFound() + } + + return ( + <div className="container py-6 space-y-6"> + {/* 헤더 */} + <div className="flex justify-between items-center"> + <div className="flex items-center gap-4"> + <div> + <h1 className="text-3xl font-bold tracking-tight"> + 입찰 기본 정보 관리 + </h1> + <p className="text-muted-foreground mt-2"> + 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title} + </p> + </div> + </div> + <Link href={`/${lng}/evcp/bid/${id}`} passHref> + <Button variant="outline" className="flex items-center"> + <ArrowLeft className="mr-2 h-4 w-4" /> + 입찰 관리로 돌아가기 + </Button> + </Link> + </div> + + {/* 탭 네비게이션 */} + <div> + <BiddingTabs id={id} /> + </div> + + {/* 입찰 기본 정보 에디터 */} + <BiddingBasicInfoEditor biddingId={parsedId} /> + </div> + ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx new file mode 100644 index 00000000..5b686a1c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx @@ -0,0 +1,76 @@ +import { notFound } from 'next/navigation'
+import { getBiddingById } from "@/lib/bidding/service"
+import { Bidding } from "@/db/schema/bidding"
+import { Button } from "@/components/ui/button"
+import { ArrowLeft } from "lucide-react"
+import Link from "next/link"
+import { BiddingItemsEditor } from "@/components/bidding/manage/bidding-items-editor"
+import { BiddingTabs } from "../bidding-tabs"
+
+// 메타데이터 생성
+export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) {
+ const { lng, id } = await params
+ const parsedId = parseInt(id)
+ if (isNaN(parsedId)) return { title: '입찰 품목 관리' }
+
+ try {
+ const bidding = await getBiddingById(parsedId)
+ return {
+ title: bidding ? `${bidding.title} - 입찰 품목 관리` : '입찰 품목 관리',
+ }
+ } catch {
+ return { title: '입찰 품목 관리' }
+ }
+}
+
+interface PageProps {
+ params: Promise<{ lng: string; id: string }>
+}
+
+export default async function BiddingItemsPage({ params }: PageProps) {
+ const { lng, id } = await params
+ const parsedId = parseInt(id)
+
+ if (isNaN(parsedId)) {
+ notFound()
+ }
+
+ const bidding: Bidding | null = await getBiddingById(parsedId)
+
+ if (!bidding) {
+ notFound()
+ }
+
+ return (
+ <div className="container py-6 space-y-6">
+ {/* 헤더 */}
+ <div className="flex justify-between items-center">
+ <div className="flex items-center gap-4">
+ <div>
+ <h1 className="text-3xl font-bold tracking-tight">
+ 입찰 품목 관리
+ </h1>
+ <p className="text-muted-foreground mt-2">
+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title}
+ </p>
+ </div>
+
+ </div>
+ <Link href={`/${lng}/evcp/bid/${id}`} passHref>
+ <Button variant="outline" className="flex items-center">
+ <ArrowLeft className="mr-2 h-4 w-4" />
+ 입찰 관리로 돌아가기
+ </Button>
+ </Link>
+ </div>
+
+ {/* 탭 네비게이션 */}
+ <div>
+ <BiddingTabs id={id} />
+ </div>
+
+ {/* 입찰 품목 에디터 */}
+ <BiddingItemsEditor biddingId={parsedId} />
+ </div>
+ )
+}
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx deleted file mode 100644 index 80e7f8d2..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { getBiddingById, getBiddingConditions } from "@/lib/bidding/service" -import { Bidding } from "@/db/schema/bidding" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import Link from "next/link" -import { BiddingInfoHeader } from "@/components/bidding/bidding-info-header" -import { BiddingConditionsEdit } from "@/components/bidding/bidding-conditions-edit" -export const metadata: Metadata = { - title: "Bidding Detail", -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string , id: string} -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 입찰 정보 조회 - const bidding: Bidding | null = await getBiddingById(idAsNumber) - const biddingConditions = await getBiddingConditions(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "입찰 사전견적", - href: `/${lng}/evcp/bid/${id}/pre-quote`, - }, - { - title: "입찰 관리상세", - href: `/${lng}/evcp/bid/${id}/detail`, - }, - ] - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - {/* RFQ 목록으로 돌아가는 링크 추가 */} - <div className="flex justify-between items-center mb-4"> - <div> - {/* 4) 입찰 정보가 있으면 번호 + 제목 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {bidding - ? `입찰 No. ${bidding.biddingNumber ?? ""} - ${bidding.title}` - : "Loading Bidding..."} - </h2> - </div> - <Link href={`/${lng}/evcp/bid`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>입찰 목록으로 돌아가기</span> - </Button> - </Link> - </div> - - {/* 입찰 정보 헤더 */} - <BiddingInfoHeader bidding={bidding} /> - - {/* 입찰 조건 */} - {bidding && ( - <BiddingConditionsEdit - biddingId={bidding.id} - initialConditions={biddingConditions} - /> - )} - - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 overflow-auto max-w-full">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx deleted file mode 100644 index ca0788a5..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { redirect } from 'next/navigation' - -interface PageProps { - params: Promise<{ lng: string; id: string }> -} - -export default async function Page({ params }: PageProps) { - const { lng, id } = await params - - // 기본적으로 입찰 사전견적 페이지로 리다이렉트 - redirect(`/${lng}/evcp/bid/${id}/pre-quote`) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx deleted file mode 100644 index d978974b..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Suspense } from 'react' -import { notFound } from 'next/navigation' -import { getBiddingDetailData } from '@/lib/bidding/detail/service' -import { getBiddingCompanies } from '@/lib/bidding/pre-quote/service' -import { BiddingPreQuoteContent } from '@/lib/bidding/pre-quote/table/bidding-pre-quote-content' - -// 메타데이터 생성 -export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params - const parsedId = parseInt(id) - if (isNaN(parsedId)) return { title: '입찰 사전견적' } - - try { - const detailData = await getBiddingDetailData(parsedId) - return { - title: detailData.bidding ? `${detailData.bidding.title} - 입찰 사전견적` : '입찰 사전견적', - } - } catch { - return { title: '입찰 사전견적' } - } -} - -interface PageProps { - params: Promise<{ id: string }> -} - -export default async function Page({ params }: PageProps) { - const { id } = await params - const parsedId = parseInt(id) - - if (isNaN(parsedId)) { - notFound() - } - - // 통합 데이터 로딩 함수 사용 - const detailData = await getBiddingDetailData(parsedId) - - if (!detailData.bidding) { - notFound() - } - - // 사전견적용 입찰 업체들 조회 - const biddingCompaniesResult = await getBiddingCompanies(parsedId) - const biddingCompanies = biddingCompaniesResult?.success ? biddingCompaniesResult.data || [] : [] - - return ( - <Suspense fallback={<div className="p-8">로딩 중...</div>}> - <BiddingPreQuoteContent - bidding={detailData.bidding} - quotationDetails={detailData.quotationDetails} - biddingCompanies={biddingCompanies} - prItems={detailData.prItems} - /> - </Suspense> - ) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx new file mode 100644 index 00000000..a79bef88 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx @@ -0,0 +1,73 @@ +import { notFound } from 'next/navigation'
+import { getBiddingById } from "@/lib/bidding/service"
+import { Bidding } from "@/db/schema/bidding"
+import { Button } from "@/components/ui/button"
+import { ArrowLeft } from "lucide-react"
+import Link from "next/link"
+import { BiddingScheduleEditor } from "@/components/bidding/manage/bidding-schedule-editor"
+import { BiddingTabs } from "../bidding-tabs"
+
+// 메타데이터 생성
+export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) {
+ const { lng, id } = await params
+ const parsedId = parseInt(id)
+ if (isNaN(parsedId)) return { title: '입찰 일정 관리' }
+
+ try {
+ const bidding = await getBiddingById(parsedId)
+ return {
+ title: bidding ? `${bidding.title} - 입찰 일정 관리` : '입찰 일정 관리',
+ }
+ } catch {
+ return { title: '입찰 일정 관리' }
+ }
+}
+
+interface PageProps {
+ params: Promise<{ lng: string; id: string }>
+}
+
+export default async function BiddingSchedulePage({ params }: PageProps) {
+ const { lng, id } = await params
+ const parsedId = parseInt(id)
+
+ if (isNaN(parsedId)) {
+ notFound()
+ }
+
+ const bidding: Bidding | null = await getBiddingById(parsedId)
+
+ if (!bidding) {
+ notFound()
+ }
+
+ return (
+ <div className="container py-6 space-y-6">
+ {/* 헤더 */}
+ <div className="flex justify-between items-center">
+ <div>
+ <h1 className="text-3xl font-bold tracking-tight">
+ 입찰 일정 관리
+ </h1>
+ <p className="text-muted-foreground mt-2">
+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title}
+ </p>
+ </div>
+ <Link href={`/${lng}/evcp/bid/${id}`} passHref>
+ <Button variant="outline" className="flex items-center">
+ <ArrowLeft className="mr-2 h-4 w-4" />
+ 입찰 관리로 돌아가기
+ </Button>
+ </Link>
+ </div>
+
+ {/* 탭 네비게이션 */}
+ <div>
+ <BiddingTabs id={id} />
+ </div>
+
+ {/* 입찰 일정 에디터 */}
+ <BiddingScheduleEditor biddingId={parsedId} />
+ </div>
+ )
+}
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx index aa9f33b5..973593d8 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx @@ -4,14 +4,9 @@ import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" import { getBiddings, getBiddingStatusCounts, - getBiddingTypeCounts, - getBiddingManagerCounts, - getBiddingMonthlyStats, - getUserCodeByEmail, } from "@/lib/bidding/service" import { searchParamsCache } from "@/lib/bidding/validation" import { BiddingsPageHeader } from "@/lib/bidding/list/biddings-page-header" -import { BiddingsStatsCards } from "@/lib/bidding/list/biddings-stats-cards" import { BiddingsTable } from "@/lib/bidding/list/biddings-table" import { getValidFilters } from "@/lib/data-table" import { type SearchParams } from "@/types/table" @@ -33,30 +28,13 @@ export default async function BiddingsPage(props: IndexPageProps) { const validFilters = getValidFilters(search.filters) - // ✅ 입찰 데이터를 먼저 가져옴 - const biddingsResult = await getBiddings({ - ...search, - filters: validFilters, - }) - - // ✅ 입찰 데이터에 managerCode 추가 - const biddingsDataWithManagerCode = await Promise.all( - biddingsResult.data.map(async (item) => { - let managerCode: string | null = null - if (item.managerEmail) { - managerCode = await getUserCodeByEmail(item.managerEmail) - } - return { ...item, managerCode: managerCode || null } - }) - ) - // ✅ 모든 데이터를 병렬로 로드 const promises = Promise.all([ - Promise.resolve({ ...biddingsResult, data: biddingsDataWithManagerCode }), + getBiddings({ + ...search, + filters: validFilters, + }), getBiddingStatusCounts(), - getBiddingTypeCounts(), - getBiddingManagerCounts(), - getBiddingMonthlyStats(), ]) return ( @@ -67,13 +45,6 @@ export default async function BiddingsPage(props: IndexPageProps) { <BiddingsPageHeader /> {/* ═══════════════════════════════════════════════════════════════ */} - {/* 통계 카드들 */} - {/* ═══════════════════════════════════════════════════════════════ */} - <Suspense fallback={<BiddingsStatsCardsSkeleton />}> - <BiddingsStatsCardsWrapper promises={promises} /> - </Suspense> - - {/* ═══════════════════════════════════════════════════════════════ */} {/* 메인 테이블 */} {/* ═══════════════════════════════════════════════════════════════ */} <Suspense @@ -92,44 +63,3 @@ export default async function BiddingsPage(props: IndexPageProps) { </Shell> ) } - -// ═══════════════════════════════════════════════════════════════ -// 통계 카드 래퍼 컴포넌트 -// ═══════════════════════════════════════════════════════════════ -async function BiddingsStatsCardsWrapper({ - promises -}: { - promises: Promise<[ - Awaited<ReturnType<typeof getBiddings>>, - Awaited<ReturnType<typeof getBiddingStatusCounts>>, - Awaited<ReturnType<typeof getBiddingTypeCounts>>, - Awaited<ReturnType<typeof getBiddingManagerCounts>>, - Awaited<ReturnType<typeof getBiddingMonthlyStats>>, - ]> -}) { - const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats] = await promises - - return ( - <BiddingsStatsCards - total={biddingsResult.total} - statusCounts={statusCounts} - typeCounts={typeCounts} - managerCounts={managerCounts} - monthlyStats={monthlyStats} - /> - ) -} - -// 통계 카드 스켈레톤 -function BiddingsStatsCardsSkeleton() { - return ( - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> - {Array.from({ length: 4 }).map((_, i) => ( - <div key={i} className="rounded-lg border p-6"> - <div className="h-4 bg-muted rounded animate-pulse mb-2" /> - <div className="h-8 bg-muted rounded animate-pulse" /> - </div> - ))} - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx index 003db012..09ce13e7 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx @@ -1,38 +1,24 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { BiddingNoticeEditor } from '@/lib/bidding/bidding-notice-editor' -import { getBiddingNoticeTemplate } from '@/lib/bidding/service' +import { BiddingNoticeTemplateManager } from '@/lib/bidding/bidding-notice-template-manager' +import { getBiddingNoticeTemplates } from '@/lib/bidding/service' // template 받을 때, 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. // 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getBiddingNoticeTemplate 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. export const dynamic = 'force-dynamic' export default async function BiddingNoticePage() { - const template = await getBiddingNoticeTemplate() + const templates = await getBiddingNoticeTemplates() return ( <div className="container mx-auto py-6 max-w-6xl"> <div className="mb-6"> - <h1 className="text-3xl font-bold tracking-tight">입찰공고문 관리</h1> + <h1 className="text-3xl font-bold tracking-tight">입찰공고문 템플릿 관리</h1> <p className="text-muted-foreground mt-2"> - 표준 입찰공고문 템플릿을 작성하고 관리할 수 있습니다. + 입찰공고문 템플릿을 타입별로 작성하고 관리할 수 있습니다. + 각 타입별 템플릿은 입찰 생성 시 기본 양식으로 사용됩니다. </p> </div> - <Card> - <CardHeader> - <CardTitle>표준 입찰공고문 템플릿</CardTitle> - <CardDescription> - 이 템플릿은 실제 입찰 공고 작성 시 기본 양식으로 사용됩니다. - 필요한 표준 정보와 서식을 미리 작성해두세요. - </CardDescription> - </CardHeader> - <CardContent> - <BiddingNoticeEditor - initialData={template} - /> - </CardContent> - </Card> + <BiddingNoticeTemplateManager initialTemplates={templates} /> </div> ) }
\ No newline at end of file |
